#!/usr/bin/python
# encoding: utf-8
#
# Copyright 2010-2014 Greg Neagle.
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
iconutils

Created by Greg Neagle on 2014-05-15.

Functions to work with product images ('icons') for Managed Software Center
"""

import glob
import sys
import os

import subprocess
import tempfile
import shutil

from Foundation import NSData
from AppKit import NSBitmapImageRep, NSPNGFileType
import munkicommon
import FoundationPlist


def convertIconToPNG(icon_path, destination_path, desired_pixel_height=350):
    '''Converts an icns file to a png file, choosing the representation
    closest to (but >= if possible) the desired_pixel_height.
    Returns True if successful, False otherwise'''
    if os.path.exists(icon_path):
        image_data = NSData.dataWithContentsOfFile_(icon_path)
        bitmap_reps = NSBitmapImageRep.imageRepsWithData_(image_data)
        chosen_rep = None
        for bitmap_rep in bitmap_reps:
            if not chosen_rep:
                chosen_rep = bitmap_rep
            elif (bitmap_rep.pixelsHigh() >= desired_pixel_height
                  and bitmap_rep.pixelsHigh() < chosen_rep.pixelsHigh()):
                chosen_rep = bitmap_rep
        if chosen_rep:
            png_data = chosen_rep.representationUsingType_properties_(
                NSPNGFileType, None)
            png_data.writeToFile_atomically_(destination_path, False)
            return True
    return False


def findIconForApp(app_path):
    '''Finds the icon file for app_path. Returns a path or None.'''
    if not os.path.exists(app_path):
        return None
    try:
        info = FoundationPlist.readPlist(
            os.path.join(app_path, u'Contents/Info.plist'))
    except (FoundationPlist.FoundationPlistException):
        return None
    app_name = os.path.basename(app_path)
    icon_filename = info.get('CFBundleIconFile', app_name)
    icon_path = os.path.join(app_path, u'Contents/Resources', icon_filename)
    if not os.path.splitext(icon_path)[1]:
        # no file extension, so add '.icns'
        icon_path += '.icns'
    if os.path.exists(icon_path):
        return icon_path
    return None


def extractAppBitsFromPkgArchive(archive_path, target_dir):
    '''Extracts application Info.plist and .icns files into target_dir
       from a package archive file. Returns the result code of the
       pax extract operation.'''
    result = -999
    if os.path.exists(archive_path):
        original_dir = os.getcwd()
        os.chdir(target_dir)
        cmd = ['/bin/pax', '-rzf', archive_path,
               '*.app/Contents/Info.plist',
               '*.app/Contents/Resources/*.icns']
        proc = subprocess.Popen(cmd, shell=False, bufsize=-1,
                               stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                               stderr=subprocess.STDOUT)
        (output, errors) = proc.communicate()
        result = proc.returncode
        os.chdir(original_dir)
    return result


def extractAppIconsFromFlatPkg(pkg_path):
    '''Extracts application icons from a flat package.
       Returns a list of paths to icns files.'''
    cmd = ['/usr/sbin/pkgutil', '--bom', pkg_path]
    proc = subprocess.Popen(cmd, shell=False, bufsize=-1,
                            stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                            stderr=subprocess.STDOUT)
    (output, errors) = proc.communicate()
    if proc.returncode:
        print >> sys.stderr, u'Could not get bom files from %s' % pkg_path
        return []
    bomfilepaths = output.splitlines()
    pkg_dict = {}
    for bomfile in bomfilepaths:
        # bomfile path is of the form:
        # /tmp/FlashPlayer.pkg.boms.2Rxa1z/AdobeFlashPlayerComponent.pkg/Bom
        pkgname = os.path.basename(os.path.dirname(bomfile))
        if not pkgname.endswith(u'.pkg'):
            # no subpackages; this is a component pkg
            pkgname = ''
        cmd = ['/usr/bin/lsbom', '-s', bomfile]
        proc = subprocess.Popen(cmd, shell=False, bufsize=-1,
                                stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                                stderr=subprocess.STDOUT)
        (output, errors) = proc.communicate()
        if proc.returncode:
            print >> sys.stderr, u'Could not lsbom %s' % bomfile
        # record paths to all app Info.plist files
        pkg_dict[pkgname] = [os.path.normpath(line)
            for line in output.decode('utf-8').splitlines()
            if line.endswith(u'.app/Contents/Info.plist')]
        if not pkg_dict[pkgname]:
            # remove empty lists
            del(pkg_dict[pkgname])
    if not pkg_dict:
        return []
    icon_paths = []
    pkgtmp = os.path.join(tempfile.mkdtemp(dir=u'/tmp'), u'pkg')
    exporttmp = tempfile.mkdtemp(dir='/tmp')
    cmd = ['/usr/sbin/pkgutil', '--expand', pkg_path, pkgtmp]
    result = subprocess.call(cmd)
    if result == 0:
        for pkg in pkg_dict:
            archive_path = os.path.join(pkgtmp, pkg, u'Payload')
            err = extractAppBitsFromPkgArchive(archive_path, exporttmp)
            if err == 0:
                for info_path in pkg_dict[pkg]:
                    full_path = os.path.join(exporttmp, info_path)
                    # convert path to Info.plist to path to app
                    app_path = os.path.dirname(os.path.dirname(full_path))
                    icon_path = findIconForApp(app_path)
                    if icon_path:
                        icon_paths.append(icon_path)
            else:
                print >> sys.stderr, (
                    u'pax could not read files from %s' % archive_path)
                return []
    else:
        print >> sys.stderr, u'Could not expand %s' % pkg_path
    # clean up our expanded flat package; we no longer need it
    shutil.rmtree(pkgtmp)
    return icon_paths


def findInfoPlistPathsInBundlePkg(pkg_path):
    '''Returns a dict with pkg paths as keys and filename lists
    as values'''
    pkg_dict = {}
    bomfile = os.path.join(pkg_path, u'Contents/Archive.bom')
    if os.path.exists(bomfile):
        info_paths = getAppInfoPathsFromBundleComponentPkg(pkg_path)
        if info_paths:
            pkg_dict[pkg_path] = info_paths
    else:
        # mpkg or dist pkg; look for component pkgs within
        pkg_dict = {}
        original_dir = os.getcwd()
        pkg_contents_dir = os.path.join(pkg_path, u'Contents')
        if os.path.isdir(pkg_contents_dir):
            os.chdir(pkg_contents_dir)
            pkgs = (glob.glob('*.pkg') + glob.glob('*/*.pkg')
                    + glob.glob('*/*/*.pkg') + glob.glob('*.mpkg') +
                    glob.glob('*/*.mpkg') + glob.glob('*/*/*.mpkg'))
            os.chdir(original_dir)
        else:
            pkgs = []
        for pkg in pkgs:
            full_path = os.path.join(pkg_contents_dir, pkg)
            pkg_dict.update(findInfoPlistPathsInBundlePkg(full_path))
    return pkg_dict


def extractAppIconsFromBundlePkg(pkg_path):
    '''Returns a list of paths for application icons found
    inside the bundle pkg at pkg_path'''
    pkg_dict = findInfoPlistPathsInBundlePkg(pkg_path)
    icon_paths = []
    exporttmp = tempfile.mkdtemp(dir='/tmp')
    for pkg in pkg_dict:
        archive_path = os.path.join(pkg, u'Contents/Archive.pax.gz')
        err = extractAppBitsFromPkgArchive(archive_path, exporttmp)
        if err == 0:
            for info_path in pkg_dict[pkg]:
                full_path = os.path.normpath(os.path.join(exporttmp, info_path))
                app_path = os.path.dirname(os.path.dirname(full_path))
                icon_path = findIconForApp(app_path)
                if icon_path:
                    icon_paths.append(icon_path)
    return icon_paths


def getAppInfoPathsFromBundleComponentPkg(pkg_path):
    '''Returns a list of paths to application Info.plists'''
    bomfile = os.path.join(pkg_path, u'Contents/Archive.bom')
    if os.path.exists(bomfile):
        cmd = ['/usr/bin/lsbom', '-s', bomfile]
        proc = subprocess.Popen(cmd, shell=False, bufsize=-1,
                                stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                                stderr=subprocess.STDOUT)
        (output, errors) = proc.communicate()
        return [line for line in output.splitlines()
                if line.endswith('.app/Contents/Info.plist')]
    return []
